<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

// namespace \spica\core\net;

/**
 * Implementation of a basic HTTP client that supports both client to
 * server communication.
 *
 * namespace \spica\core\net\HttpClient;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.1
 * @since      February 18, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaHttpClient
{
    /**
     * Timeout (in seconds) for complete download process including all file transfer (default 5 minutes)
     *
     * @var int
     */
    public $timeout = 300;

    /**
     * Timeout (in seconds) for connection to server; this is the timeout that
     * usually happens if the remote server is completely down (default 20 seconds);
     * may not work when using proxy
     *
     * @var int
     */
    public $connectTimeout = 20;

    /**
     * HTTP proxy.
     *
     * @var SpicaHttpProxy
     */
    protected $_proxy;

    /**
     * Request IP.
     *
     * @var string
     */
    protected $_ip;

    /**
     * Is session closed?
     *
     * @var bool
     */
    protected $_isClosed = false;

    /**
     * Is session closed by user?
     *
     * @var bool
     */
    protected $_isClosedExplicitly = false;

    /**
     * Curl executor.
     *
     * @var SpicaCurlExecutor
     */
    private $_executor;

    /**
     * Constructs an object of <code>SpicaHttpClient</code>
     *
     * @param SpicaCurlExecutor $executor
     */
    public function __construct($executor = null)
    {
        $this->_executor = $executor;
    }

    /**
     * Sets proxy.
     *
     * @param SpicaHttpProxy $proxy
     */
    public function setProxy($proxy)
    {
        $this->_proxy = $proxy;
    }

    /**
     * Sets IP.
     *
     * The interface name, ip address, or host name must belong to the local machine.
     *
     * This settings will affect $_SERVER['REMOTE_ADDRESS'] in server.
     *
     * @param string $ip
     */
    public function setInterface($ip)
    {
        $this->_ip = $ip;
    }

    /**
     * Gets local machine's interface name, IP or host name.
     *
     * @return string
     */
    public function getInterface()
    {
        return $this->_ip;
    }

    /**
     * Sends an HTTP request to a given URL based on the current configuration.
     *
     * @throws Exception when an error occurs
     * @param  SpicaHttpRequest $request the Http request
     * @return SpicaHttpResponse
     */
    public function sendRequest($request)
    {
        if (true === $this->_isClosed)
        {
            if (true === $this->_isClosedExplicitly)
            {
                throw new Exception('This session has been closed by invoking close() explicitly before.');
            }

            throw new Exception('This session has been closed.');
        }

        if (null === $this->_executor)
        {
            $this->_executor = new SpicaCurlExecutor();
        }

        $content = $this->_executor->execute($request, null, $this->timeout, $this->connectTimeout, $this->_ip);
        $session = $this->_executor->getSession();
        $response = new SpicaHttpResponse($session);
        $response->setBody($content);
        $errorCode = $this->_executor->getErrorCode();

        if ($errorCode > 0)
        {
            throw new SpicaHttpClientException($this->_executor->getErrorMessage(), $errorCode);
        }

        $this->_executor->free();
        return $response;
    }

    /**
     * Closes the session explicitly.
     */
    public function close()
    {
        $this->_isClosed = true;
        $this->_isClosedExplicitly = $explicitly;
        $this->_session = null;
        $this->_executor->close();
    }

    /**
     * Tests if session is closed?
     *
     * @return bool
     */
    public function isClosed()
    {
        return $this->_isClosed;
    }
}

/**
 * Basic implementation of HTTP messages consist of requests from client to server
 * and responses from server to client.
 *
 * namespace \spica\core\net\HttpMessage;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      December 15, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
abstract class SpicaHttpMessage
{
    /**
     * Indicates HTTP version 1.0.
     *
     * @var int
     */
    const HTTP_1_0 = CURL_HTTP_VERSION_1_0;

    /**
     * Indicates HTTP version 1.1.
     *
     * @var int
     */
    const HTTP_1_1 = CURL_HTTP_VERSION_1_1;

    /**
     * HTTP response version
     *
     * @var int Default to HTTP 1.1
     */
    public $httpVersion = CURL_HTTP_VERSION_1_1;

    /**
     * HTTP status code
     *
     * @var int
     */
    public $httpCode;

    /**
     * HTTP headers
     *
     * @var array
     */
    public $headers;

    /**
     * Retrieves the field for a given header (utility method).
     *
     * @param string $name
     * @return string|false
     */
    public function getHeader($name)
    {
        if (true === isset($this->headers[$name]))
        {
            return $this->headers[$name];
        }

        return false;
    }

    /**
     * Removes all headers with a certain name from this message.
     *
     * @param string $name
     */
    public function removeHeaders($name)
    {
        if (true === isset($this->headers[$name]))
        {
            unset($this->headers[$name]);
        }
    }

    /**
     * Checks if a certain header is present in this message.
     *
     * @param string $name
     * @return bool
     */
    public function containsHeader($name)
    {
        return isset($this->headers[$name]);
    }
}

/**
 * HTTP messages consist of requests from client to server.
 *
 * namespace \spica\core\net\HttpRequest;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.1
 * @since      February 18, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaHttpRequest extends SpicaHttpMessage
{
    /**
     * HTTP method "GET".
     *
     * @var int
     */
    const METHOD_GET = CURLOPT_HTTPGET;

    /**
     * HTTP method "POST".
     *
     * @var int
     */
    const METHOD_POST = CURLOPT_POST;

    /**
     * HTTP method "HEAD".
     *
     * @var int
     */
    const METHOD_HEAD = CURLOPT_NOBODY;

    /**
     * HTTP method "PUT".
     *
     * @var string
     */
    const METHOD_PUT = 'PUT';

    /**
     * HTTP method "DELETE".
     *
     * @var string
     */
    const METHOD_DELETE = 'DELETE';

    /**
     * HTTP method "TRACE".
     *
     * @var string
     */
    const METHOD_TRACE = 'TRACE';

    /**
     * HTTP authentication method which is an alias to
     * CURLAUTH_BASIC | CURLAUTH_DIGEST | CURLAUTH_GSSNEGOTIATE | CURLAUTH_NTLM.
     *
     * @var int
     */
    const AUTH_ANY = CURLAUTH_ANY;

    /**
     * HTTP authentication method which aliases to
     * CURLAUTH_DIGEST|CURLAUTH_GSSNEGOTIATE|CURLAUTH_NTLM.
     *
     * @var int
     */
    const AUTH_ANYSAFE = CURLAUTH_ANYSAFE;

    /**
     * HTTP authentication method: BASIC.
     *
     * @var int
     */
    const AUTH_BASIC = CURLAUTH_BASIC;

    /**
     * HTTP authentication method: DIGEST.
     *
     * @var int
     */
    const AUTH_DIGEST = CURLAUTH_DIGEST;

    /**
     * HTTP authentication method: GSS.
     *
     * @var int
     */
    const AUTH_GSS = CURLAUTH_GSSNEGOTIATE;

    /**
     * HTTP authentication method: NTLM.
     *
     * @var int
     */
    const AUTH_NTLM = CURLAUTH_NTLM;

    /**
     * Is return content required?
     *
     * @var int
     */
    public $contentRequired = true;

    /**
     * Is return header required?
     *
     * @var int
     */
    public $headerRequired = false;

    /**
     * Tracking sent headers or not for debugging purpose?
     *
     * @var bool
     */
    public $storeSentHeaders = true;

    /**
     * Maximum redirect response: 301, 302
     *
     * @var int
     */
    public $maxRedirects = 3;

    /**
     * Does HTTP client follow redirection?
     *
     * @var int
     */
    public $followLocation = 1;

    /**
     * HTTP method in use for the request.
     *
     * @var int Default to GET
     */
    protected $_method = CURLOPT_HTTPGET;

    /**
     * HTTP authentication method in use for the request.
     *
     * @var int Default to null which means no authentication
     */
    protected $_authMethod;

    /**
     * Request URL.
     *
     * @var string
     */
    protected $_url;

    /**
     * User agent
     *
     * @var string
     */
    protected $_userAgent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_7; en-us) AppleWebKit/533.4 (KHTML, like Gecko) Version/4.1 Safari/533.4';

    /**
     * HTTP headers.
     *
     * @var array
     */
    protected $_headers = array();

    /**
     * User credentials.
     *
     * @var array [user => '', pass => '']
     */
    protected $_credentials;

    /**
     * Proxy user credentials.
     *
     * @var array [user => '', pass => '']
     */
    protected $_proxyCredentials;

    /**
     * POST data.
     *
     * @var array
     */
    protected $_post = array();

    /**
     * POST files.
     *
     * @var array
     */
    protected $_postFiles = array();

    /**
     * File to upload.
     *
     * @var string
     */
    protected $_filePath;

    /**
     * Construct an object of <code>SpicaHttpRequest</code>
     */
    public function __construct() {}

    /**
     * Sets HTTP method.
     *
     * @param int $method
     */
    public function setMethod($method)
    {
        $this->_method = $method;
    }

    /**
     * Gets HTTP method.
     *
     * @return int
     */
    public function getMethod()
    {
        return $this->_method;
    }

    /**
     * Sets POST data.
     *
     * @param array $data An associtive array
     */
    public function setPostData($data)
    {
        $this->_post = $data;
    }

    /**
     * Gets POST data.
     *
     * @return array An associative array
     */
    public function getPostData()
    {
        return $this->_post;
    }

    /**
     * Adds a file to upload together with the form.
     *
     * @param string $fieldName
     * @param string $path
     */
    public function attachFile($fieldName, $path)
    {
        $this->_postFiles[][$fieldName] = '@'.$path;
    }

    /**
     * Gets POST files.
     *
     * @return array An array
     */
    public function getAttachedFiles()
    {
        return $this->_postFiles;
    }

    /**
     * Gets request URL.
     *
     * @return string
     */
    public function getUrl()
    {
        return $this->_url;
    }

    /**
     * Sets request URL.
     *
     * @param string $url
     */
    public function setUrl($url)
    {
        if (0 !== stripos($url, 'http://', 0))
        {
            $url = 'http://'.$url;
        }

        $this->_url = $url;
    }

    /**
     * Sets user agent header.
     *
     * @param $ua User agent string
     */
    public function setUserAgent($ua)
    {
        $this->_userAgent = $ua;
    }

    /**
     * Gets user agent header.
     *
     * @return string the user agent string
     */
    public function getUserAgent()
    {
        return $this->_userAgent;
    }

    /**
     * Sets HTTP headers.
     *
     * @param array $headers
     */
    public function setHeaders($headers)
    {
        $this->_headers = $headers;
    }

    /**
     * Gets HTTP headers.
     *
     * @return array
     */
    public function getHeaders()
    {
        return $this->_headers;
    }

    /**
     * Uses HTTP headers as if it is a normal user agent.
     */
    public function simulateBrowser()
    {
        $this->_headers = array(
          'User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6',
          'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
          'Accept-Language: en-us,en;q=0.5',
          'Accept-Encoding: gzip,deflate',
          'Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7',
          'Keep-Alive: 300',
          'Connection: keep-alive',
          'Referer: http://google.com',
          'Cache-Control: max-age=0',
        );
    }

    /**
     * Sets user authentication credentials.
     *
     * @param string $user
     * @param string $password
     */
    public function setCredentials($user, $password)
    {
        $this->_credentials['user'] = $user;
        $this->_credentials['pass'] = $password;
    }

    /**
     * Gets user credentials.
     *
     * @return array Null if there is no credentials or an array if it is.
     */
    public function getCredentials()
    {
        return $this->_credentials;
    }

    /**
     * Sets proxy user authentication credentials.
     *
     * @param string $user
     * @param string $password
     */
    public function setProxyCredentials($user, $password)
    {
        $this->_proxyCredentials['user'] = $user;
        $this->_proxyCredentials['pass'] = $password;
    }

    /**
     * Gets proxy user credentials.
     *
     * @return array Null if there is no credentials or an array if it is.
     */
    public function getProxyCredentials()
    {
        return $this->_proxyCredentials;
    }

    /**
     * Sets HTTP authentication method.
     *
     * @param int $method
     */
    public function setAuthenticationMethod($method)
    {
        $this->_authMethod = $method;
    }

    /**
     * Gets authentication method.
     *
     * @return int
     */
    public function getAuthenticationMethod()
    {
        return $this->_authMethod;
    }

    /**
     * Sets file path and HTTP method to upload it using PUT.
     *
     * @param string $filePath
     */
    public function setUploadFile($filePath)
    {
        $this->_filePath = $filePath;
    }

    /**
     * Gets upload file.
     *
     * @return string
     */
    public function getUploadFile()
    {
        return $this->_filePath;
    }
}

/**
 * Wrapper which provides access to the components of an HTTP response.
 *
 * namespace \spica\core\net\HttpResponse;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.1
 * @since      February 18, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaHttpResponse extends SpicaHttpMessage
{
    /**
     * HTTP body
     *
     * @var string
     */
    protected $_body;

    /**
     * Response header.
     *
     * @var string|false
     */
    protected $_header = false;

    /**
     * Curl executed session.
     *
     * @var resource of type (curl)
     */
    protected $_session;

    /**
     * Constructs an object of <code>SpicaHttpResponse</code>.
     *
     * @param resource $session Resource of type (curl) which represents Curl executed session handle
     */
    public function __construct(&$session)
    {
        $this->_session = &$session;
    }

    /**
     * Sets response body.
     *
     * @param string $body
     */
    public function setBody($body)
    {
        if (true === is_string($body))
        {
            $this->_body = $body;
        }
    }

    /**
     * Gets response body.
     *
     * @return string
     */
    public function getBody()
    {
        return $this->_body;
    }

    /**
     * Gets HTTP status code returned by the server if the HTTP request is is made successfully.
     *
     * @return string
     */
    public function getHttpCode()
    {
        return curl_getinfo($this->_session, CURLINFO_HTTP_CODE);
    }

    /**
     * Gets HTTP request URL.
     *
     * @return string
     */
    public function getUrl()
    {
        return curl_getinfo($this->_session, CURLINFO_EFFECTIVE_URL);
    }

    /**
     * Gets sent HTTP headers.
     *
     * @see    SpicaHttpRequest::storeSentHeaders must be true
     * @return string|false
     */
    public function getSentHttpHeaders()
    {
        return curl_getinfo($this->_session, CURLINFO_HEADER_OUT);
    }

    /**
     * Gets response HTTP headers.
     *
     * @return string|false
     */
    public function getHttpHeaders()
    {
        return $this->_header;
    }

    /**
     * Gets response HTTP headers.
     *
     * @param string $header
     */
    public function setHttpHeaders($header)
    {
        $this->_header = $header;
    }

    /**
     * Gets the value of the HTTP Content-type header.
     *
     * @return string
     */
    public function getContentType()
    {
        return curl_getinfo($this->_session, CURLINFO_CONTENT_TYPE);
    }
}

/**
 * Basic implementation of HTTP proxy that locates in the middle between client and server.
 *
 * namespace \spica\core\net\HttpProxy;
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      December 15, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaHttpProxy
{
    public $host;
    public $port;
}

/**
 * An utility class that provides basic option setting in Curl.
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      March 29, 2010
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaCurlExecutor
{
    /**
     * Temporary file handle.
     * 
     * @var resource
     */
    private $_tempFile;

    /**
     * Curl session
     *
     * @var resource of type (curl)
     */
    private $_session;

    /**
     * Constructs an object of <code>SpicaCurlExecutor</code>
     */
    public function __construct() {}

    /**
     * Executes Curl handle based on a SpicaHttpRequest.
     *
     * @param SpicaHttpRequest $request
     * @param resource $session Curl session
     * @param int $timeout
     * @param int $connectTimeout
     * @param string $ip
     * @return null|string
     */
    public function execute($request, $session = null, $timeout = 30, $connectTimeout = 30, $ip = null)
    {
        if (null === $session)
        {
            $session = curl_init();
        }
        
        $this->prepare($request, $session, $timeout, $connectTimeout, $ip);
        $content = null;
        
        if (true === $request->contentRequired)
        {
            $content = curl_exec($session);
        }
        else
        {
            curl_exec($session);
        }

        $this->_session = $session;
        return $content;
    }

    /**
     * Prepares Curl handle based on a SpicaHttpRequest.
     *
     * @param SpicaHttpRequest $request
     * @param resource $session Curl session
     * @param int $timeout
     * @param int $connectTimeout
     * @param string $ip
     * @return resource Curl session
     */
    public function prepare($request, $session, $timeout, $connectTimeout, $ip = null)
    {
        curl_setopt($session, CURLOPT_CONNECTTIMEOUT, $connectTimeout);
        curl_setopt($session, CURLOPT_FOLLOWLOCATION, $request->followLocation);
        curl_setopt($session, CURLOPT_TIMEOUT, $timeout);
        // ask for headers in the response output
        curl_setopt($session, CURLOPT_HEADER, (int) $request->headerRequired);
        // ask for the response output as the return value
        curl_setopt($session, CURLOPT_RETURNTRANSFER, (int) $request->contentRequired);
        curl_setopt($session, CURLINFO_HEADER_OUT, $request->storeSentHeaders);
        // Accept-Encoding: gzip,deflate
        curl_setopt($session, CURLOPT_ENCODING, "gzip,deflate");
        curl_setopt($session, CURLOPT_MAXREDIRS, $request->maxRedirects);

        if (null !== $ip)
        {
            // host interface can be $_SERVER['SERVER_ADDR']
            curl_setopt($session, CURLOPT_INTERFACE, $ip);
        }

        curl_setopt($session, CURLOPT_URL, $request->getUrl());
        curl_setopt($session, CURLOPT_HTTP_VERSION, $request->httpVersion);

        $headers = $request->getHeaders();

        if (false === empty($headers))
        {
            curl_setopt($session, CURLOPT_HTTPHEADER, $headers);
        }

        curl_setopt($session, CURLOPT_USERAGENT, $request->getUserAgent());
        // HTTP method in use
        $method = $request->getMethod();

        switch ($method)
        {
            case SpicaHttpRequest::METHOD_POST:
                curl_setopt($session, CURLOPT_POST, true);
                $files = $request->getAttachedFiles();

                if (count($files) > 1)
                {
                    curl_setopt($session, CURLOPT_POSTFIELDS, $request->getPostData());
                }
                else
                {
                    curl_setopt($session, CURLOPT_POSTFIELDS, array_merge($request->getPostData(), isset($files[0]) ? $files[0] : array()));
                }

                break;

            case SpicaHttpRequest::METHOD_HEAD:
                curl_setopt($session, CURLOPT_NOBODY, 1);
                break;

            case SpicaHttpRequest::METHOD_PUT:
                curl_setopt($session, CURLOPT_PUT, 1);
                $filePath = $request->getUploadFile();

                if (null !== $filePath)
                {
                    $tempFile = tmpfile(); // tempory file pointer
                    $content = file_get_contents($filePath);
                    fwrite($tempFile, $content);
                    fseek($tempFile, 0);
                    curl_setopt($session, CURLOPT_INFILE, $tempFile);
                    curl_setopt($session, CURLOPT_INFILESIZE, strlen($content));
                    $this->_tempFile = $tempFile;
                }

                break;

            default: // GET
                break;
        }

        if (null !== ($credentials = $request->getCredentials()))
        {
            curl_setopt($session, CURLOPT_USERPWD, "{$credentials['user']}:{$credentials['pass']}");

            if (null === ($authMethod = $request->getAuthenticationMethod()))
            {
                $request->setAuthenticationMethod(SpicaHttpRequest::AUTH_BASIC);
                $authMethod = SpicaHttpRequest::AUTH_BASIC;
            }

            curl_setopt($session, CURLOPT_HTTPAUTH, $authMethod);
        }
    }

    /**
     * Gets session handle.
     *
     * @return null|resource of type (curl)
     */
    public function getSession()
    {
        return $this->_session;
    }

    /**
     * Gets session error number (not related to HTTP status code).
     *
     * @return int Error code {@link http://curl.haxx.se/libcurl/c/libcurl-errors.html}
     */
    public function getErrorCode()
    {
        if (null !== $this->_session)
        {
            return curl_errno($this->_session);
        }

        return null;
    }

    /**
     * Gets session error message (not related to HTTP status message).
     *
     * @return string
     */
    public function getErrorMessage()
    {
        if (null !== $this->_session)
        {
            return curl_error($this->_session);
        }

        return null;
    }


    /**
     * Free any temporary resource created by this builder.
     */
    public function free()
    {
        // Close temporary file if any
        if (null !== $this->_tempFile)
        {
            fclose($this->_tempFile);
        } 
    }

    /**
     * Closes Curl handle.
     */
    public function close()
    {
        if (null !== $this->_session)
        {
            curl_close($this->_session);
        }
    }
}

/**
 * Exception caused by input/output failures such as an unreliable connection
 * or an inability to complete the execution of an HTTP method within the given
 * time constraint (socket timeout) or logical errors caused by a mismatch between
 * the client and the server (web server or proxy server) in their interpretation
 * of the HTTP specification.
 *
 * @category   spica
 * @package    core
 * @subpackage net
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      December 15, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: HttpClient.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaHttpClientException extends Exception {}

?>